const { TOKEN_TYPE, KEY_WORD } = require('./JackTokenizer.js');

const NON_TERMINALS = {
  CLASS: 'class',
  CLASS_VAR_DEC: 'classVarDec',
  SUBROUTINE_DEC: 'subroutineDec',
  SUBROUTINE_BODY: 'subroutineBody',
  PARAMETER_LIST: 'parameterList',
  VAR_DEC: 'varDec',
  STATEMENTS: 'statements',
  LET: 'letStatement',
  IF: 'ifStatement',
  WHILE: 'whileStatement',
  DO: 'doStatement',
  RETURN: 'returnStatement',
  EXPRESSION: 'expression',
  TERM: 'term',
  EXPRESSION_LIST: 'expressionList'
};

class CompilationEngine {
  constructor(tokenizer) {
    this.tokenizer = tokenizer;
    this.tokens = [];
    this.parseTree = [];
  }

  generateParseTree(tokens) {
    this.tokens = tokens.slice();
    this.parseTree = [];
    this.compileClass();
  }

  get currentToken() {
    return this.tokens[0];
  }

  get nextToken() {
    return this.tokens[1];
  }

  _process(type, strs = []) {
    const token = this.tokens.shift();
    const isTypeOk = token.type === type;
    const isStrOk = typeof strs === 'string' ? strs === token.str : strs.length === 0 || strs.includes(token.str);
    if (isTypeOk && isStrOk) {
      return {
        ...token,
        tag: token.type
      };
    }
    this._syntaxError(`(type: ${token.type}, str: ${token.str}) should be (type: ${type}, str: ${strs})`);
  }

  /**
   * 'class' className '{' classVarDec* subroutineDec* '}'
   */
  compileClass() {
    const node = { tag: NON_TERMINALS.CLASS };
    this.parseTree.push(node);
    const children = [];
    children.push(this._process(TOKEN_TYPE.KEYWORD, KEY_WORD.CLASS));
    children.push(this._process(TOKEN_TYPE.IDENTIFIER));
    children.push(this._process(TOKEN_TYPE.SYMBOL, '{'));
    while (['static', 'field'].includes(this.currentToken.str)) {
      children.push(this.compileClassVarDec());
    }
    while (['constructor', 'function', 'method'].includes(this.currentToken.str)) {
      children.push(this.compileSubroutineDec());
    }
    children.push(this._process(TOKEN_TYPE.SYMBOL, '}'));
    node.children = children;
  }

  /**
   * ('static'|'field') type varName (',', varName)* ';'
   */
  compileClassVarDec() {
    const node = { tag: NON_TERMINALS.CLASS_VAR_DEC };
    const children = [];
    children.push(this._process(TOKEN_TYPE.KEYWORD, ['static', 'field']));
    children.push(this._processType());
    children.push(this._process(TOKEN_TYPE.IDENTIFIER));
    while (this.currentToken && this.currentToken.str === ',') {
      children.push(this._process(TOKEN_TYPE.SYMBOL, ','));
      children.push(this._process(TOKEN_TYPE.IDENTIFIER));
    }
    children.push(this._process(TOKEN_TYPE.SYMBOL, ';'));
    node.children = children;
    return node;
  }

  /**
   * ('constructor'|'function'|'method') ('void'|type) subroutineName '(' parameterList ')' subroutineBody
   */
  compileSubroutineDec() {
    const node = { tag: NON_TERMINALS.SUBROUTINE_DEC };
    const children = [];
    children.push(this._process(TOKEN_TYPE.KEYWORD, ['constructor', 'function', 'method']));
    if (this.currentToken.str === 'void') {
      children.push(this._process(TOKEN_TYPE.KEYWORD, 'void'));
    } else {
      children.push(this._processType());
    }
    children.push(this._process(TOKEN_TYPE.IDENTIFIER));
    children.push(this._process(TOKEN_TYPE.SYMBOL, '('));
    children.push(this.compileParameterList());
    children.push(this._process(TOKEN_TYPE.SYMBOL, ')'));
    children.push(this.compileSubroutineBody());
    node.children = children;
    return node;
  }

  /**
   * ((type varName) (',' type varName)*)?
   */
  compileParameterList() {
    const node = { tag: NON_TERMINALS.PARAMETER_LIST };
    const children = [];
    if (this.currentToken.str !== ')') {
      children.push(this._processType());
      children.push(this._process(TOKEN_TYPE.IDENTIFIER));
      while (this.currentToken && this.currentToken.str === ',') {
        children.push(this._process(TOKEN_TYPE.SYMBOL, ','));
        children.push(this._processType());
        children.push(this._process(TOKEN_TYPE.IDENTIFIER));
      }
    }
    node.children = children;
    return node;
  }

  /**
   * '{' varDec* statements '}'
   */
  compileSubroutineBody() {
    const node = { tag: NON_TERMINALS.SUBROUTINE_BODY };
    const children = [];
    children.push(this._process(TOKEN_TYPE.SYMBOL, '{'));
    while (this.currentToken.str === 'var') {
      children.push(this.compileVarDec());
    }
    children.push(this.compileStatements());
    children.push(this._process(TOKEN_TYPE.SYMBOL, '}'));
    node.children = children;
    return node;
  }

  /**
   * 'var' type varName (',' varName)* ';'
   */
  compileVarDec() {
    const node = { tag: NON_TERMINALS.VAR_DEC };
    const children = [];
    children.push(this._process(TOKEN_TYPE.KEYWORD, 'var'));
    children.push(this._processType());
    children.push(this._process(TOKEN_TYPE.IDENTIFIER));
    while (this.currentToken && this.currentToken.str === ',') {
      children.push(this._process(TOKEN_TYPE.SYMBOL, ','));
      children.push(this._process(TOKEN_TYPE.IDENTIFIER));
    }
    children.push(this._process(TOKEN_TYPE.SYMBOL, ';'));
    node.children = children;
    return node;
  }

  /**
   * statement*
   */
  compileStatements() {
    const node = { tag: NON_TERMINALS.STATEMENTS };
    const children = [];
    const statementType = ['let', 'if', 'while', 'do', 'return'];
    while (statementType.includes(this.currentToken.str)) {
      const { str } = this.currentToken;
      if (str === 'let') {
        children.push(this.compileLet());
      } else if (str === 'if') {
        children.push(this.compileIf());
      } else if (str === 'while') {
        children.push(this.compileWhile());
      } else if (str === 'do') {
        children.push(this.compileDo());
      } else if (str === 'return') {
        children.push(this.compileReturn());
      }
    }
    node.children = children;
    return node;
  }

  /**
   * 'let' varName ('[' expression ']')? '=' expression ';'
   */
  compileLet() {
    const node = { tag: NON_TERMINALS.LET };
    const children = [];
    children.push(this._process(TOKEN_TYPE.KEYWORD, 'let'));
    children.push(this._process(TOKEN_TYPE.IDENTIFIER));
    if (this.currentToken.str === '[') {
      children.push(this._process(TOKEN_TYPE.SYMBOL, '['));
      children.push(this.compileExpression());
      children.push(this._process(TOKEN_TYPE.SYMBOL, ']'));
    }
    children.push(this._process(TOKEN_TYPE.SYMBOL, '='));
    children.push(this.compileExpression());
    children.push(this._process(TOKEN_TYPE.SYMBOL, ';'));
    node.children = children;
    return node;
  }

  /**
   * 'if' '(' expression ')' '{' statements '}' ('else' '{' statements '}')?
   */
  compileIf() {
    const node = { tag: NON_TERMINALS.IF };
    const children = [];
    children.push(this._process(TOKEN_TYPE.KEYWORD, 'if'));
    children.push(this._process(TOKEN_TYPE.SYMBOL, '('));
    children.push(this.compileExpression());
    children.push(this._process(TOKEN_TYPE.SYMBOL, ')'));
    children.push(this._process(TOKEN_TYPE.SYMBOL, '{'));
    children.push(this.compileStatements());
    children.push(this._process(TOKEN_TYPE.SYMBOL, '}'));
    if (this.currentToken.str === 'else') {
      children.push(this._process(TOKEN_TYPE.KEYWORD, 'else'));
      children.push(this._process(TOKEN_TYPE.SYMBOL, '{'));
      children.push(this.compileStatements());
      children.push(this._process(TOKEN_TYPE.SYMBOL, '}'));
    }
    node.children = children;
    return node;
  }

  /**
   * 'while' '(' expression ')' '{' statements '}'
   */
  compileWhile() {
    const node = { tag: NON_TERMINALS.WHILE };
    const children = [];
    children.push(this._process(TOKEN_TYPE.KEYWORD, 'while'));
    children.push(this._process(TOKEN_TYPE.SYMBOL, '('));
    children.push(this.compileExpression());
    children.push(this._process(TOKEN_TYPE.SYMBOL, ')'));
    children.push(this._process(TOKEN_TYPE.SYMBOL, '{'));
    children.push(this.compileStatements());
    children.push(this._process(TOKEN_TYPE.SYMBOL, '}'));
    node.children = children;
    return node;
  }

  /**
   * 'do' subroutineCall ';'
   */
  compileDo() {
    const node = { tag: NON_TERMINALS.DO };
    const children = [];
    children.push(this._process(TOKEN_TYPE.KEYWORD, 'do'));
    children.push(...this.compileTerm().children); // 使用 compileTerm 间接得到 subroutineCall 的数据
    children.push(this._process(TOKEN_TYPE.SYMBOL, ';'));
    node.children = children;
    return node;
  }

  /**
   * 'return' expression? ';'
   */
  compileReturn() {
    const node = { tag: NON_TERMINALS.RETURN };
    const children = [];
    children.push(this._process(TOKEN_TYPE.KEYWORD, 'return'));
    if (this.currentToken.str !== ';') {
      children.push(this.compileExpression());
    }
    children.push(this._process(TOKEN_TYPE.SYMBOL, ';'));
    node.children = children;
    return node;
  }

  /**
   * term (op term)*
   * op: '+'| '-' | '*' | '/' | '&' | '|' | '<' | '>' | '='
   */
  compileExpression() {
    const node = { tag: NON_TERMINALS.EXPRESSION };
    const children = [];
    const op = ['+', '-', '*', '/', '&', '|', '<', '>', '='];
    children.push(this.compileTerm());
    while (op.includes(this.currentToken.str)) {
      children.push(this._process(TOKEN_TYPE.SYMBOL, op));
      children.push(this.compileTerm());
    }
    node.children = children;
    return node;
  }

  /**
   * integerConstant|stringConstant|keywordConstant|varName|varName'['expression']'|'('expression')'|(unaryOp term)|subroutineCall
   * subroutineCall: subroutineName'('expressionList')'|(className|varName)'.'subroutineName'('expressionList')'
   * unaryOp: '-' | '~'
   * keywordConstant: 'true' | 'false' | 'null' | 'this'
   */
  compileTerm() {
    const node = { tag: NON_TERMINALS.TERM };
    const children = [];
    const { type, str } = this.currentToken;
    if (type === TOKEN_TYPE.INT_CONST) {
      children.push(this._process(TOKEN_TYPE.INT_CONST));
    } else if (type === TOKEN_TYPE.STRING_CONST) {
      children.push(this._process(TOKEN_TYPE.STRING_CONST));
    } else if (['true', 'false', 'null', 'this'].includes(str)) {
      children.push(this._process(TOKEN_TYPE.KEYWORD, ['true', 'false', 'null', 'this']));
    } else if (str === '(') {
      children.push(this._process(TOKEN_TYPE.SYMBOL, '('));
      children.push(this.compileExpression());
      children.push(this._process(TOKEN_TYPE.SYMBOL, ')'));
    } else if (['-', '~'].includes(str)) {
      children.push(this._process(TOKEN_TYPE.SYMBOL, ['-', '~']));
      children.push(this.compileTerm());
    } else if (type === TOKEN_TYPE.IDENTIFIER) {
      if (this.nextToken.str === '[') {
        // varName'['expression']'
        children.push(this._process(TOKEN_TYPE.IDENTIFIER));
        children.push(this._process(TOKEN_TYPE.SYMBOL, '['));
        children.push(this.compileExpression());
        children.push(this._process(TOKEN_TYPE.SYMBOL, ']'));
      } else if (this.nextToken.str === '(') {
        // subroutineName'('expressionList')'
        children.push(this._process(TOKEN_TYPE.IDENTIFIER));
        children.push(this._process(TOKEN_TYPE.SYMBOL, '('));
        children.push(this.compileExpressionList());
        children.push(this._process(TOKEN_TYPE.SYMBOL, ')'));
      } else if (this.nextToken.str === '.') {
        // (className|varName)'.'subroutineName'('expressionList')'
        children.push(this._process(TOKEN_TYPE.IDENTIFIER));
        children.push(this._process(TOKEN_TYPE.SYMBOL, '.'));
        children.push(this._process(TOKEN_TYPE.IDENTIFIER));
        children.push(this._process(TOKEN_TYPE.SYMBOL, '('));
        children.push(this.compileExpressionList());
        children.push(this._process(TOKEN_TYPE.SYMBOL, ')'));
      } else {
        // varName
        children.push(this._process(TOKEN_TYPE.IDENTIFIER));
      }
    }
    node.children = children;
    return node;
  }

  /**
   * (expression (',' expression)*)?
   */
  compileExpressionList() {
    const node = { tag: NON_TERMINALS.EXPRESSION_LIST };
    const children = [];
    if (this.currentToken.str !== ')') {
      children.push(this.compileExpression());
      while (this.currentToken.str === ',') {
        children.push(this._process(TOKEN_TYPE.SYMBOL, ','));
        children.push(this.compileExpression());
      }
    }
    node.children = children;
    return node;
  }

  _processType() {
    if (this.currentToken.type === TOKEN_TYPE.KEYWORD) {
      return this._process(TOKEN_TYPE.KEYWORD, ['int', 'char', 'boolean']);
    } else {
      return this._process(TOKEN_TYPE.IDENTIFIER);
    }
  }

  _syntaxError(msg) {
    throw new Error(msg);
  }

  convertParseTreeToXml() {
    const result = [];
    this.parseTree.forEach((node) => {
      this._nodeToXml(node, result, 0);
    });
    return result.concat('').join('\n');
  }

  convertParseTreeToJson() {
    return JSON.stringify(this.parseTree);
  }

  _nodeToXml(node, result, indent) {
    const { tag, children, str } = node;
    const prefix = Array(indent).fill(' ').join('');
    if (children) {
      result.push(`${prefix}<${tag}>`);
      children.forEach((child) => {
        this._nodeToXml(child, result, indent + 2);
      });
      result.push(`${prefix}</${tag}>`);
    } else {
      result.push(`${prefix}<${tag}> ${this._escapeChar(str)} </${tag}>`);
    }
  }

  _escapeChar(str) {
    if (typeof str === 'string') {
      return str.replace('&', '&amp;').replace('>', '&gt;').replace('<', '&lt;').replace('"', '&quot;');
    }
    return str;
  }
}

module.exports.CompilationEngine = CompilationEngine;
